-- MarketM
-- Created by chengb Jan/27/2015
-- 商城模块
-- 一些必要的说明
-- 1、开放充值的运营平台配置在公式： GET_RECHARGE_PLATFORMS 中
-- 2、只要是充值类的商品，都要在market.csv中配置sku（商品库存标识），充值类商品只能通过sdk购买
-- 3、商城充值开关关闭后，充值功能暂不可用，所有充值类的商品都不会出现在商城中

module("MarketM", package.seeall);

-- 配置
local marketTable = {};
local goodsByType = {};
local goodsByAlias = {};
local goodsHeroList = {};
local skuList = {};
local limitTimeGift = {};

-- sku商品实际价格
local skuPriceList = {};

-- 常量声明
local SUPERIOR_ID          = 5001;     -- 月卡ID
local GROUP_BUY_ID         = 5003;     -- 团购ID
local GEM_BONUS            = 5002;     -- 钻石消费
local SLIME_RANK           = 5004;     -- 冈布奥星阶
local KILL_BOSS_ID         = 5005;     -- 击杀BOSS奖励ID
local OPEN_DUNGEON         = 11;       -- 开放商城的通关关卡

-- 商品美元价格表
local GOODS_PRICE_MAP = {
    [10001] = 0.99,
    [10002] = 9.99,
    [10003] = 16.99,
    [10004] = 16.99,
    [10005] = 17.99,
    [10006] = 59.99,
    [10007] = 2.99,
    [10008] = 99.99,
    [5001]  = 9.99,
    [5008]  = 6.99,
};

-- 内部函数声明
local whenDismissAdScreen;
local whenLoginOk;
local whenRechargeFail;
local whenRechargeOk;

-- 载入配置表
local _init = false;
local loadCsv = function()
    if _init then return end
    _init = true;

    marketTable = {};
    goodsByType = {};
    goodsByAlias = {};
    goodsHeroList = {};
    skuList = {};
    limitTimeGiftTable = {};

    -- 载入商城配置
    local market_CSV = get_market_CSV();
    for i = 1,  #market_CSV do
        -- 解析出商品信息
        local id = market_CSV[i].id;
        local classify = market_CSV[i].classify;

        -- 解析商品奖励
        marketTable[id] = parseBonus(market_CSV[i]);

        -- 归类
        goodsByType[classify] = goodsByType[classify] or {};
        table.insert(goodsByType[classify], marketTable[id]);

        if classify == "hero" then
            -- 如果是冈布奥，记录一下
            table.insert(goodsHeroList, marketTable[id].bonus[1][2]);
        end

        -- 别名
        local alias = market_CSV[i].alias;
        goodsByAlias[alias] = marketTable[id];

        -- 如果是内购商品，需要记录起来
        local sku = querySku(id);
        if sku ~= nil and not isPtRechargeGoods(id) then
            table.insert(skuList, sku);
        end
    end
    market_CSV = {};

    -- 载入限时礼包表
    local limit_time_CSV = get_limit_time_gift_CSV();
    for i = 1, #limit_time_CSV do
        local id = limit_time_CSV[i].id;

        limitTimeGiftTable[id] = limit_time_CSV[i];
    end
    limit_time_CSV = {};
end

-- 模块初始化
function init()
    loadCsv();

    -- 初始化充值类商品价格表
    skuPriceList = {};

    EventMgr.removeAll("MarketM");

    -- 注册玩家登录的回调
    EventMgr.register("MarketM", event.LOGIN_OK, whenLoginOk);

    -- 注册充值失败的回调
    EventMgr.register("MarketM", event.RECHARGE_FAIL, whenRechargeFail);

    -- 注册充值扣款成功的回调
    EventMgr.register("MarketM", event.RECHARGE_OK, whenRechargeOk);

    -- 玩家通关回调
    EventMgr.register("MarketM", event.DUNGEON_PASS, function(para)
        local dungeonId = para["dungeonId"];
        if isRechargeOpen() and dungeonId == OPEN_DUNGEON then
            -- 尝试获取价格
            tryRequestSkuPrice();
        end
    end);
end

-- 解析商品奖励
function parseBonus(info)
    -- 目前解析后的bonus字段仅支持string和table两种格式
    -- 如果是string类型，则调用公式执行奖励，否则直接执行奖励
    local prefix = "formula_";
    if string.startWith(info.bonus, prefix) then
        -- 需要调用公式执行奖励
        local formulaName = string.sub(info.bonus, string.len(prefix) + 1);
        info.bonus = formulaName;
    else
        -- 解析奖励
        local arr = string.explode(info.bonus, "+");
        if #arr == 0 then
            -- 获取不到商品的奖励信息
            return info;
        end

        local strBonus;
        local classId;
        local amount;
        local ret = {};
        for _, strBonus in pairs(arr) do
            local temp;
            if string.startWith(strBonus, "1(") and string.endWith(strBonus, ")") then
                -- 物品奖励
                temp = string.explode(string.sub(strBonus, 3, -2), ",");
                table.insert(ret, { 1, tonumber(temp[1]), tonumber(temp[2]) });
            elseif string.startWith(strBonus, "2(") and string.endWith(strBonus, ")") then
                -- 属性奖励
                temp = string.explode(string.sub(strBonus, 3, -2), ",");
                table.insert(ret, { 2, temp[1], tonumber(temp[2]) });
            elseif string.startWith(strBonus, "3(") and string.endWith(strBonus, ")") then
                -- 英雄奖励
                temp = string.explode(string.sub(strBonus, 3, -2), ",");
                table.insert(ret, { 3, tonumber(temp[1]), tonumber(temp[2]) });
            elseif string.startWith(strBonus, "6(") and string.endWith(strBonus, ")") then
                -- 秘宝奖励
                temp = string.explode(string.sub(strBonus, 3, -2), ",");
                table.insert(ret, { 6, tonumber(temp[1]), tonumber(temp[2]) });
            else
                assert(false, "商品bonus字段配置格式有误，商品编号：" .. info.id);
            end
        end

        info.bonus = ret;
    end

    return info;
end

-- 检索某个商品的配置
function query(id, path)
    local m = marketTable[id]

    if not m or path == "" or not path then
        return m;
    end

    return m[path];
end

-- 检索限时礼包的配置
function queryTimeLimitGift(id, path)
    local m = limitTimeGiftTable[id];

    if not m or path == "" or not path then
        return m
    end

    return m[path];
end

-- 检索商品库存标识
function querySku(id)
    -- 仅内购的商品有sku
    local skuMap = query(id, "sku");
    if type(skuMap) ~= "table" then
        return nil;
    end

    -- 通过pt充值
    if isPtRechargeGoods(id) then
        return skuMap["default_sku"];
    end

    if type(PUBLISH_PLATFORM) ~= "string" then
        return nil;
    end

    -- 返回相应运营平台的配置（默认为青瓷平台）
    return skuMap[PUBLISH_PLATFORM] or skuMap[PUBLISH_TYPE_QC];
end

-- 获取所有商品库存标识
function getSkuList()
    return skuList;
end

-- 查询商品信息
function getGoodsInfo(id)
    return marketTable[id];
end

-- 查询商品价格
function queryGoodsPrice(id)
    if not marketTable[id] then
        return nil;
    end

    -- 暂时返回配置表中的价格
    return marketTable[id]["price"];
end

-- 根据商品类别获取商品
function getGoodsByType(type)
    return goodsByType[type];
end

-- 设置充值开关
function setRechargeEnabled(enabled)
    ME.user.dbase:set("is_recharge_enabled", enabled);

    if isRechargeOpen() then
        -- 尝试获取价格
        tryRequestSkuPrice();
    end

    -- 抛出事件
    EventMgr.fire(event.RECHARGE_ENABLED, { ["enabled"] = enabled, });
end

-- 充值开关是否开启（注：这里仅判断充值开关，如果要判断充值功能是否开启，请使用isRechargeOpen接口）
function isRechargeEnabled()
    return ME.user.dbase:query("is_recharge_enabled") == 1;
end

-- 根据类别获得未售罄的商品
function getSellGoldsByType(goodsType)
    local finalList = {};
    local list = goodsByType[goodsType] or {};

    for _, info in pairs(list) do
        -- 相当于continue
        while true do
            local goodsId = info.id;

            if isRmbBuy(goodsId) and not isRechargeOpen() then
                -- 如果是充值类的商品，需要开启充值功能时才显示
                break;
            end

            if isSoldOut(goodsId) then
                -- 已销售完，则不添加
                break;
            end

            -- 如果是史莱姆类商品，并且玩家应经拥有该史莱姆，则不能再购买
            if info["classify"] == "hero" then
                local bonus = info["bonus"];
                local petId = bonus[1][2];
                if PetM.getMyPet(petId) then
                    break;
                end
            end

            -- TODO 如果是番外篇，并且玩家已经开启过，则不能再购买
            if info["classify"] == "dlc" then
                local bonusArgs = info["bonus_args"];
                local areaId = bonusArgs["area"][1];
                if DungeonAreaM.isDungeonOpened(ME.user, areaId) or not isValidGoods(goodsId) then
                    break;
                end
            end

            -- 月卡未过期不添加
            if goodsId == SUPERIOR_ID and not SuperiorM.canActiveSuperior() then
                break;
            end

            if goodsId == GROUP_BUY_ID then
                break;
            end

            -- 未开启关卡不显示击杀boss选项
            if goodsId == KILL_BOSS_ID and not GiftM.isShowKillBoss() then
                break;
            end

            -- 没达到第一档，消费/星阶不显示
            if goodsId == SLIME_RANK then
                if not GiftM.canTakeGift("slime", 1) and not GiftM.beAlreadyTook("slime", 1) then
                    break;
                end
            elseif goodsId == GEM_BONUS then
                if not GiftM.canTakeGift("gem", 1) and not GiftM.beAlreadyTook("gem", 1) then
                    break;
                end
            end

            -- 检查显示条件
            local showCondition = query(goodsId, "show_condition");

            if showCondition ~= "" and "string" == type(showCondition) then
                local ret = FormulaM.invoke(showCondition);
                if not ret then
                    break;
                end
            end

            table.insert(finalList, info);
            break;
        end
    end

    return finalList;
end

-- 获取根据类型获取用于页面显示的未售罄商品
function getShowGoodsByType(goodsType)
    -- 根据类型获取所有符合条件的商品
    local list = getSellGoldsByType(goodsType);
    -- 过滤一下
    local showList = {};
    for _, good in pairs(list) do
        if good["alias"] ~= "superior" and good["alias"] ~= "time_gift" then
            table.insert(showList, good);
        end
    end

    return showList;
end

-- 获得充值选项
function getRechargeList()
    local finalList = {};
    local list = goodsByType["recharge"];

    for _, info in pairs(list) do
        local goodsId = info.id;
        if not isSoldOut(goodsId) then
            -- 检查显示条件
            local showCondition = query(goodsId, "show_condition");
            if "string" == type(showCondition) and showCondition ~= "" then
                local ret = FormulaM.invoke(showCondition);
                if ret then
                    table.insert(finalList, info);
                end
            else
                table.insert(finalList, info);
            end
        end
    end
    return finalList;
end

-- 获取可售的碎片列表
-- @param isAddActivityScrap 是否是活动副本
-- @param distinguishCamp    是否区分阵营
function getSellScrapList(isAddActivityScrap, distinguishCamp)
    local list = getGoodsByType("scrap");

    local finalList = {};
    for _, info in pairs(list) do
        -- 若玩家已有该冈布奥，则加入碎片
        local pieceId = info.bonus[1][2];
        local petId = PetM.getPetidByPieceid(pieceId);
        if ME.user.pets[petId] then
            if distinguishCamp == true then
                local camp = iif(CampM.getPetCamp(petId) == -1, 1, CampM.getPetCamp(petId));
                finalList[camp] = finalList[camp] or {};
                table.insert(finalList[camp], info);
            else
                table.insert(finalList, info);
            end
        end
    end

    -- 活动副本
    if isAddActivityScrap == true and not distinguishCamp then
        finalList = table.append(finalList, getActivityList());
    end

    return finalList;
end

-- 根据商品别名获取商品id
function getGoodsIdByAlias(alias)
    local m = goodsByAlias[alias];

    if not m then
        return;
    end

    return m.id;
end

------------------------------
-- 根据sku获取商品id
-- @param sku 商品库存标识
-- @param return 商品编号
function getGoodsIdBySku(sku)
    for _, info in pairs(marketTable) do
        if querySku(info.id) == sku then
            return info.id;
        end
    end

    return nil;
end

-- 判断某商品是否已售完
function isSoldOut(goodsId)
    local marketBuyInfo = ME.user.dbase:query("market_buy");
    if marketBuyInfo and marketBuyInfo[goodsId] and
        1 == marketBuyInfo[goodsId]["sold_out"] then
        -- 商品已售完
        return true;
    end

    return false;
end

--------------------------------
-- 判断能否购买
-- @param goodsId    商品id
-- @return 如果可以购买，返回true，否则返回错误码
function canBuyGoods(goodsId)
    local goodsInfo = getGoodsInfo(goodsId);
    if not goodsInfo then
        -- 商品不存在
        return false;
    end

    -- 调用公式判断
    local fomulaName = goodsInfo["check_formula"];
    local checkArgs  = goodsInfo["check_args"];
    if type(fomulaName) == "string" and string.len(fomulaName) > 0 then
        local ret = FormulaM.invoke(fomulaName, ME.user, goodsId, checkArgs);
        if true ~= ret or type(ret) == "string" then
            trace("MarketM", "经公式判断，无法购买商品(%d)", goodsId);
            return ret;
        end
    end

    -- 判断商品是否已售完
    if isSoldOut(goodsId) then
        -- 商品已售完
        return "sold_out";
    end

    if not isValidGoods(goodsId) then
        -- 非法商品
        return false;
    end

    local price = queryGoodsPrice(goodsId);
    if not price then
        -- 获取不到商品价格
        return false;
    end

    -- 判断属性是否足够支付
    local attrib = table.keys(price)[1];
    local value  = table.values(price)[1];

    if attrib ~= "rmb" and ME.user:queryAttrib(attrib) < value then
        -- 属性不足
        return attrib .. "_lack";
    end

    return true;
end

-- 是否真钱购买
function isRmbBuy(goodsId)
    local price = MarketM.queryGoodsPrice(goodsId);
    if not price then
        return false;
    end
    local attrib = table.keys(price)[1];
    if attrib ~= "rmb" then
        return false;
    end

    return true;
end

-- 是否正在充值
function isRecharging()
    if not ME.isInGame then
        return false;
    end
    return ME.user.dbase:query("is_recharging") == true;
end

-- 设置是否正在充值
function setRechargingFlag(flag)
    if flag == true then
        ME.user.dbase:set("is_recharging", true);
    else
        ME.user.dbase:delete("is_recharging");
    end
end

-- 当前设备是否被禁止充值
function isForbidDeviceRecharge()
    return ME.user.dbase:query("forbid_device_recharge") == true;
end

------------------
-- 真钱购买
-- @param goodsId  商品id
-- @param payType  支付方式
-- @param amount   充值金额
function buyByRmb(goodsId, payType, amount)
    -- 获取商品信息
    local goodsInfo = getGoodsInfo(goodsId);
    if not goodsInfo then
        trace("MarketM", "商品(%d)不存在", goodsId);
        return;
    end

    if true ~= isRmbBuy(goodsId) then
        trace("MarketM", "商品(%d)无法使用真钱购买", goodsId);
        return;
    end

    -- 如果充值开关被关闭了
    if not MarketM.isRechargeEnabled() then
        confirm2(CONFIRM_TYPE_OK, getLocStr("slime_note"), getLocStr("recharge_close_tip"));
        return;
    end

    -- 当前设备被禁止充值
    if isForbidDeviceRecharge() then
        confirm2(CONFIRM_TYPE_OK, getLocStr("slime_note"), getDialogStr("dialog_261"), showFAQs);
        return;
    end

    -- 商品被锁住了
    if isGoodsLocked(goodsId) then
        confirm2(CONFIRM_TYPE_OK, getLocStr("slime_note"),
            string.format(getLocStr("goods_locked"), goodsInfo["name"]));
        return;
    end

    -- 可以通过pt购买的话，使用pt购买(pt充值走接下来流程)
    if isBuyGemByPt() and not isPtRechargeGoods(goodsId) then
        buyGoodsByPt(goodsId);
        return;
    end

    -- 如果在手机上，通过sdk购买
    if isAndroidPlatform() or isIphonePlatform() or isIpadPlatform() then
        local sku = querySku(goodsId);
        if not sku then
            trace("MarketM", "获取不到商品（%d）的sku配置", goodsId);
            return;
        end

        if MarketM.isRecharging() then
            trace("MarketM", "正在充值，请稍后。");
            return;
        end

        local ret = buyPurchaseBySdk(sku, payType, amount);
        if ret == true then
            -- 标记正在充值，避免重复发起请求
            setRechargingFlag(true);

            -- 标记发起过充值
            -- 这里添加标记的原因：玩家登录时，如果之前有充值未到账，则会进行充值补单
            -- 倘若这时补单失败，我们不期望弹出充值未到账的确认提示框。而如果是正常发起购买，则需要弹框
            -- 因此用这个标记来区分
            ME.user.dbase:setTemp("request_charge", 1);

            -- 重置交易
            ChargeM.resetTransation();
        end
    else
        -- 暂未开放
        doAlert(getLocStr("disable_function"));
    end
end

-- 充值功能是否开启
function isRechargeOpen()
    if isRecharging() then
        -- 如果正在充值，那就不用判断了
        return true;
    end

    if not isMarketFuncOpen() then
        -- 商城未开放
        return false;
    end

    if not isRechargeEnabled() then
        -- 充值开关未开启
        return false;
    end

    -- 判断当前运营平台是否开启充值
    local openList = FormulaM.invoke("GET_RECHARGE_PLATFORMS");
    return table.indexOf(openList, PUBLISH_PLATFORM) ~= -1;
end

-- 计算充值可获得的钻石数量
function calcChargeGem(goodsId)
    local goodsInfo = getGoodsInfo(goodsId);
    if not goodsInfo then
        return 0;
    end

    local bonus = goodsInfo["bonus"];
    if type(bonus) ~= 'table' or #bonus == 0 then
        return 0;
    end

    local gemValue = bonus[1][3];
    local multiBonus = goodsInfo["dbase"]["first_buy_multi"] or 1;
    local multiBonus = iif(getBuyTimes(goodsId) == 0, multiBonus, 1);
    return gemValue * multiBonus;
end

---------------------------------
-- 购买商品
-- @param goodsId   商品id
-- @param isForce   是否无条件购买（目前这种情况只有由服务器通知购买）
-- @param isPt      是否通过pt购买，pt购买不扣除属性
-- @param extraPara {price  购买商品的花费， currency  花费的币种,   pay_channel 支付平台}
-- @return 购买是否成功
function buyGoods(goodsId, isForce, isPt, extraPara)
    local isForce = isForce or false;

    if not isForce and true ~= canBuyGoods(goodsId) then
        -- 经检测，不能购买
        return false;
    end

    -- 收集下充值信息
    extraPara = extraPara or {};
    local payPrice   = extraPara["price"] or 0;
    local currency   = extraPara["currency"] or "";
    local payChannel = extraPara["pay_channel"] or "";

    -- 扣除属性
    local price = queryGoodsPrice(goodsId);
    if not price then
        -- 获取不到商品价格
        return false;
    end

    local attrib = table.keys(price)[1];
    local value  = table.values(price)[1];

    -- 如果是真钱购买，已经在sdk进行了扣款，这里无需再扣除属性。
    if attrib ~= "rmb" and type(attrib) == "string" and not isPt and
        not ME.user:costAttrib(attrib, value) then
        -- 属性扣除失败
        return false;
    elseif type(attrib) == "number" and not ItemM.costAmount(ME.user, attrib, value) then
        -- 道具扣除失败
        return false;
    end

    -- 执行奖励
    local goodsInfo = getGoodsInfo(goodsId);
    local bonus = goodsInfo["bonus"];
    local multiBonus = goodsInfo["dbase"]["first_buy_multi"] or 1;

    -- 只有第一次才有可能获得多倍
    multiBonus = iif(getBuyTimes(goodsId) == 0, multiBonus, 1);

    assert(type(bonus) == "string" or type(bonus) == "table");

    local realBonus = {};

    if "string" == type(bonus) then
        -- 调用公式执行奖励
        realBonus = FormulaM.invoke(bonus, ME.user, goodsInfo["bonus_args"], multiBonus, payPrice, currency);
    else
        -- 直接给予奖励
        for _, arr in pairs(bonus) do
            local count = arr[3] * multiBonus;

            -- 如果是购买钻石，需要重新计算一遍
            if arr[2] == "gem" then
                count = FormulaM.invoke("CALC_CHARGE_GEM", goodsId, isPt);
            end

            local curBonus = { arr[1], arr[2], count};

            table.insert(realBonus, curBonus);

            if false == BonusM.doBonus(curBonus, "buy_goods") then
                -- 奖励执行失败了
                trace("MarketM", "购买商品时执行奖励失败了。");
                return false;
            end
        end
    end

    -- 网页第三方充值
    local compensateGem = 0;
    if ChargeM.isWeb3rdCharge(payChannel) then
        -- 将多余的金额折算成钻石返还给玩家
        compensateGem = FormulaM.invoke("CALC_WEB_3RD_COMPENSATE_GEM", goodsId, payChannel, currency, payPrice);
        if compensateGem > 0 then
            local compensateBonus = { 2, "gem", compensateGem };
            BonusM.doBonus(compensateBonus, "web_3rd_compensate");
        end
    end

    local marketBuyInfo = ME.user.dbase:query("market_buy", {});
    if nil == marketBuyInfo[goodsId] then
        marketBuyInfo[goodsId] = {}
    end
    marketBuyInfo[goodsId]["buy_times"] = (marketBuyInfo[goodsId]["buy_times"] or 0) + 1;

    -- 如果是一次性商品，则将其标记为已售完
    if 1 == goodsInfo["one-off"] then
        marketBuyInfo[goodsId]["sold_out"] = 1;
    end
    ME.user.dbase:set("market_buy", marketBuyInfo);

    -- 统计玩家当前的充值数
    local charge_value = tonumber(query(goodsId, "charge_value"));
    ME.user:addAttrib("user_charge_amount", charge_value);

    -- 累积消费金额
    if attrib == "gem" then
        local cosumeGem = ME.user.dbase:query("market_consume_gem", 0);
        cosumeGem = cosumeGem + value;
        ME.user.dbase:set("market_consume_gem", cosumeGem);
    end

    -- 记录时间
    if MarketM.query(goodsId, "classify") == "recharge" then
        ME.user.dbase:set("last_charge_time", TimeM.getServerTime());
    end

    -- 如果是充值内购商品，需要记录最近一次购买
    if isRmbBuy(goodsId) then
        ME.user.dbase:set("last_charge_goods", goodsId);

        -- 如果商品被锁住，需要解锁
        if isGoodsLocked(goodsId) then
            unlockGoods(goodsId);
        end
    end

    -- 如果有货币码，需要记录下来
    if type(currency) == "string" and string.len(currency) > 0 then
        ME.user.dbase:set("currency", currency);
    end

    -- 抛出购买商品的事件
    EventMgr.fire(event.BUY_GOODS, { ["goodsId"] = goodsId, ["bonus"] = realBonus or {}, ["pt_buy"] = isPt, });

    if isRecharging() then
        -- 移除正在充值的标记
        setRechargingFlag(false);
        ME.user.dbase:deleteTemp("request_charge")
    end

    local retArgs = {
        ["bonus"] = realBonus,
    };
    if compensateGem > 0 then
        retArgs["compensateGem"] = compensateGem;
    end

    return retArgs;
end

-- 购买了钻石
function buyGem(orderId, goodsId)
    -- 直接发给服务器处理
    Operation.cmd_buy_gem_goods(orderId, goodsId);
end

--------------------------
-- 根据商品类别获取对玩家出售的商品
-- @param type   商品类别
function getUserGoodsByType(goodType)
    local goodsList = getGoodsByType(goodType);
    if type(goodsList) ~= "table" or #goodsList == 0 then
        return {};
    end

    local ret = {};
    for _, goodsInfo in pairs(goodsList) do
        -- 将合法的商品添加到列表中
        if isValidGoods(goodsInfo["id"]) then
            table.insert(ret, goodsInfo);
        end
    end

    return ret;
end

----------------------------------
-- 判断是否合法的商品
-- @param goodsId  商品id
function isValidGoods(goodsId)
    local goodsInfo = query(goodsId);
    if not goodsInfo then
        return false;
    end

    -- 如果已经售完，并且配置了售完后隐藏
    if isSoldOut(goodsId) and 1 == goodsInfo["hide_on_sold_out"] then
        return false;
    end

    -- 如果有前置商品，必须等前置商品售完后才能购买
    local preGoods = goodsInfo["pre_goods"];
    if type(preGoods) == "number" and false == isSoldOut(preGoods) then
        return false;
    end

    -- 判断是否能显示出来
    local showCheckFormula = goodsInfo["show_check"];
    local showCheckArgs = goodsInfo["show_check_args"];
    if showCheckFormula ~= nil and showCheckFormula ~= "" and
        not FormulaM.invoke(showCheckFormula, ME.user, showCheckArgs, goodsId) then
        return false;
    end

    -- 如果是史莱姆类商品，并且玩家应经拥有该史莱姆，则不显示
    if goodsInfo["classify"] == "hero" then
        local bonus = goodsInfo["bonus"];
        if type(bonus) ~= "table" then
            return false;
        end
        local petId = bonus[1][2];
        if PetM.getMyPet(petId) then
            return false;
        end
    end

    -- TODO 如果是番外篇，并且玩家已经开启过，则不能再购买
    if goodsInfo["classify"] == "dlc" then
        local bonusArgs = goodsInfo["bonus_args"];
        local areaId = bonusArgs["area"][1];
        if DungeonAreaM.isDungeonOpened(ME.user, areaId) then
            return false;
        end
    end

    return true;
end

----------------------------------
-- 是否商城出售的冈布奥
-- @param    classId     冈布奥的id
function isMarketSoldHero(classId)
    if table.indexOf(goodsHeroList, classId) ~= -1 then
        return true;
    end

    return false;
end

---------------------------------
-- 是否团购商品
-- @param goodsId 商品ID
function isGroupBuyGoods(goodsId)
    return GroupPurchaseM.query(goodsId) ~= nil;
end

---------------------------------
-- 获取购买次数
-- @param goodsId 商品ID
function getBuyTimes(goodsId)
    local buyInfo = ME.user.dbase:query("market_buy") or {};

    if buyInfo[goodsId] == nil then
        -- 还没买过
        return 0;
    else
        return buyInfo[goodsId]["buy_times"] or 0;
    end
end

-------------------------------
-- 判断能不能购买冈布奥碎片
function canBuySpecialGoods(goodsId, amount)
    local goodsInfo = getGoodsInfo(goodsId);
    if not goodsInfo then
        -- 商品不存在
        return false;
    end

    -- 判断商品是否已售完
    if isSoldOut(goodsId) then
        -- 商品已售完
        return "sold_out";
    end

    if not isValidGoods(goodsId) then
        -- 非法商品
        return false;
    end

    local price = queryGoodsPrice(goodsId);
    if not price then
        -- 获取不到商品价格
        return false;
    end

    -- 判断属性是否足够支付
    local attrib = table.keys(price)[1];
    local value  = table.values(price)[1];
    value = value*amount;

    if attrib ~= "rmb" and type(attrib) == "string" and ME.user:queryAttrib(attrib) < value then
        -- 属性不足
        return attrib .. "_lack";
    elseif type(attrib) == "number" and ItemM.getAmount(ME.user, attrib) < value then
        -- 道具不足
        return string.format(getLocStr("something_not_enough"), ItemM.query(attrib, "name"));
    end

    return true;
end

---------------------------------
-- 购买商品
-- @param goodsId   商品id
-- @param amount
-- @return 购买是否成功
function buySpecialGoods(goodsId, amount)
    -- 经检测，不能购买
    if true ~= canBuySpecialGoods(goodsId, amount) then
        return false;
    end

    -- 扣除属性
    local price = queryGoodsPrice(goodsId);
    if not price then
        -- 获取不到商品价格
        return false;
    end

    local attrib = table.keys(price)[1];
    local value  = table.values(price)[1];
    value = value*amount;

    -- 如果是真钱购买，已经在sdk进行了扣款，这里无需再扣除属性。
    if type(attrib) == "string" and not ME.user:costAttrib(attrib, value) then
        -- 属性扣除失败
        return false;
    elseif type(attrib) == "number" and not ItemM.costAmount(ME.user, attrib, value) then
        -- 道具扣除失败
        return false;
    end

    -- 执行奖励
    local goodsInfo = getGoodsInfo(goodsId);

    local bonus = goodsInfo["bonus"];
    assert(type(bonus) == "table");

    local desc;
    if DungeonM.isInDungeon() then
        desc = "market_buy_" .. tostring(goodsId);
    end

    for _, arr in pairs(bonus) do
        if false == BonusM.doBonus({arr[1], arr[2], arr[3]*amount}, desc) then
            -- 奖励执行失败了
            trace("MarketM", "购买商品时执行奖励失败了。");
            return false;
        end
    end

    -- 如果是一次性商品，则将其标记为已售完
    if 1 == goodsInfo["one-off"] then
        local marketBuyInfo = ME.user.dbase:query("market_buy", {});
        if nil == marketBuyInfo[goodsId] then
            marketBuyInfo[goodsId] = {}
        end
        marketBuyInfo[goodsId]["sold_out"] = 1;
        ME.user.dbase:set("market_buy", marketBuyInfo);
    end

    -- 累积消费金额
    if attrib == "gem" then
        local cosumeGem = ME.user.dbase:query("market_consume_gem", 0);
        cosumeGem = cosumeGem + value;
        ME.user.dbase:set("market_consume_gem", cosumeGem);
    end

    -- 抛出购买商品的事件
    EventMgr.fire(event.BUY_SLIME_SCRAP, { ["goodsId"] = goodsId, ["amount"] = amount});

    return true;
end

-- 商城功能是否开启
function isMarketFuncOpen()
    -- 第一小关是否通关
    return DungeonAreaM.isDungeonPassed(ME.user, OPEN_DUNGEON);
end

-- 玩家登录的回调
function whenLoginOk()
    if isIosPlatform() then
        SdkMgr.notifyMarketLoaded();
    elseif isAndroidPlatform() then

        -- 可以通过Pt购买时不需要向平台请求价格，读取配置的pt价格
        if not isBuyGemByPt() then
            -- 初始化充值类商品价格表
            skuPriceList = {};

            -- 尝试向平台请求获取sku商品价格
            ScheduleM.createScheme("MarketM", tryRequestSkuPrice, 1, true);
        end
    end
end

-- 记录sku商品价格
function recordSkuPrice(sku, price)
    if PUBLISH_PLATFORM == "AppStore" then
        -- TODO iTunes返回的人民币符号有点问题（ASCII码不正常，且包含一些奇怪的空格）这里需要特殊处理一下
        local arr = { 239, 191, 165, 194, 160 };
        local function isRmbSymbol(sym)
            for i = 1, #arr do
                if string.byte(sym, i) ~= arr[i] then
                    return false;
                end
            end
            return true;
        end

        local temp = string.sub(price, 1, #arr);
        if isRmbSymbol(temp) then
            temp = "￥";
            price = temp .. string.sub(price, #arr + 1, string.len(price));
        end
    end

    -- 过滤空格
    price = string.gsub(price, " ", "");

    cclog("记录sku商品(%s)的价格：%s", sku, price);

    skuPriceList[sku] = price;
end

-- 尝试向平台请求获取sku商品价格
function tryRequestSkuPrice()
    ScheduleM.deleteScheme("MarketM");

    local waitList = {};
    for _, sku in pairs(skuList) do
        -- 如果还未向平台获取到价格，则请求获取一次
        if skuPriceList[sku] == nil then
            table.insert(waitList, sku);
        end
    end

    if #waitList == 0 then
        return;
    end

    -- 批量查询商品价格
    batchQuerySkuPrice(waitList);
end

-- 根据sku获取商品价格
function getPriceBySku(sku)
    local isPt = isBuyGemByPt();
    if skuPriceList[sku] and not isPt then
        return skuPriceList[sku];
    end

    -- 获取不到价格时，使用默认价格
    local goodsId = getGoodsIdBySku(sku);
    if goodsId == nil then
        return "";
    end

    -- 使用pt购买时直接读取配置中的pt价格
    if isPt then
        local ptPrice = query(goodsId, "pt_price");
        return ptPrice["pt"] .. " pt";
    else
        local dbase = query(goodsId, "dbase");
        return dbase["default_show_price"];
    end
end

-- 获得豪华礼包的别名
function getSuperiorAlias()
    local ret = {};
    local list = goodsByType["superior"];

    for k, info in pairs(list) do
        local aliasName = info.alias;
        table.insert(ret, aliasName);
    end

    return ret;
end

-- 获得当前碎片的价格
function getCurScrapPrice(classId)
    local list = table.append(getGoodsByType("scrap"),
        getGoodsByType("activity"));

    local value = 0;
    local attrib = "gem";   -- 默认是钻石属性的

    for _, info in pairs(list) do
        -- 若玩家已有该冈布奥，则加入碎片
        local pieceId = info.bonus[1][2];
        if pieceId == classId then
            local price = info.price;
            if type(price) ~= "table" then
                -- 获取价格失败
                return {attrib, value};
            end
            attrib = table.keys(info.price)[1];
            value = info.price[attrib];
            return {attrib, value};
        end
    end

    return {attrib, value};
end

-- 判断有没有充过值
function haveEverRecharged()
    local haveRecharged = true;

    -- 获取最近一次付费的时间
    local lastTime = ME.user.dbase:query("last_charge_time");
    if type(lastTime) ~= "number" or lastTime == 0 then
        -- 如果从未充值过
        haveRecharged = false;
    end

    return haveRecharged;
end

-- 能否领取了首充奖励
function canTakeFirstChargeBonus()
    local firstCharge = ME.user.dbase:query("first_recharge_bonus") or 0;

    -- 1为已领取
    if firstCharge == 1 then
        return false;
    end

    -- 未充值不可领取
    if not haveEverRecharged() then
        return false;
    end

    return true;
end

-- 领取首充奖励
function takeFirstCharge(bonusList)
    if not canTakeFirstChargeBonus() then
        return false;
    end

    -- 奖励
    for _, bonus in pairs(bonusList) do
        BonusM.doBonus(bonus, "recharge_bonus");
    end

    -- 标记
    ME.user.dbase:set("first_recharge_bonus", 1);

    -- 通知服务器
    SyncM.addMessage("CMD_TAKE_FIRST_RECHARGE_BONUS");
    SyncM.startSync();

    return true;
end

-- 充值失败的回调
function whenRechargeFail()
    if isRecharging() then
        -- 取消正在充值的标记
        setRechargingFlag(false);
        ME.user.dbase:deleteTemp("request_charge")
    end
end

-- 充值扣款成功的回调
function whenRechargeOk(args)
    -- 像月卡这类商品是不能重复购买的，因此扣款成功要先锁住，直到服务器通知到账
    local sku = args.sku;
    local goodsId = getGoodsIdBySku(sku);
    local dbase = query(goodsId, "dbase");
    if dbase and dbase["avoid_repeat"] == 1 then
        lockGoods(goodsId);
    end
end

-- 锁住商品
-- 玩家无法购买被锁住的商品，需要等到服务器通知到账后再进行解锁
function lockGoods(goodsId)
    local key = string.format("charge_lock_%d", goodsId);
    UserDefaultM.setStringForKey(key, "1");
    trace("MarketM", "商品(%d)已上锁。", goodsId);
end

-- 商品是否上锁
function isGoodsLocked(goodsId)
    local key = string.format("charge_lock_%d", goodsId);
    return UserDefaultM.getStringForKey(key) == "1";
end

-- 给商品解锁
function unlockGoods(goodsId)
    if not isGoodsLocked(goodsId) then
        -- 本来就没锁住
        return;
    end
    local key = string.format("charge_lock_%d", goodsId);
    UserDefaultM.setStringForKey(key, "");
    trace("MarketM", "商品(%d)已成功解锁。", goodsId);
end

-- 限时礼包原价与现价显示修改为(60元->CNY 60.00) 其他暂时不管
-- @param price SDK返回的价格
-- @param value 超值额度（原价是现价的多少倍，这里是百分比）
function transformPriceText(price, value)
    -- 截取数字
    local yuan = getLocStr("yuan");

    local currencyStr = "%s";
    -- 以“元”结尾改成CNY开头
    if string.endWith(price, yuan) then
        currencyStr = "CNY %s";
    else
        -- 可能是以单位开头的，这边找到第一个数字，数字前的作单位
        local numAt = string.find(price, "%d");

        assert(type(numAt) == "number", "价格必须包含数字！");

        if numAt > 1 then
            currencyStr = string.sub(price, 1, numAt - 1);
            currencyStr = currencyStr .. "%s";
        end
    end

    -- 各种各样，这边按数字计算下，截取数字开头跟数字结尾的
    local numStartIndex, numEndIndex = string.find(price, "%d.*%d");

    local numStr;

    -- 可能就一个数字
    if numStartIndex == nil then
        numStartIndex, numEndIndex= string.find(price, "%d+");
        numStr = string.sub(price, numStartIndex, numEndIndex);
    else
        numStr = string.sub(price, numStartIndex, numEndIndex);
    end

    -- 注意从字符串最后开始记录非数字的位置
    local notNumMap = {};

    -- 所有数字拼接起来
    local num = "";
    local function spliceNum(numTemp)
        local numLen = string.len(numTemp);

        numStartIndex, numEndIndex = string.find(numTemp, "%D+");

        -- 如果全是数字
        if numStartIndex == nil then
            num = numTemp;

            return;
        end

        local notNum = string.sub(numTemp, numStartIndex, numEndIndex);

        -- 一开始的倒数第n位
        notNumMap[numLen - numEndIndex] = notNum;

        if numEndIndex < numLen then
            num = string.sub(numTemp, 1, numStartIndex - 1) .. string.sub(numTemp, numEndIndex + 1, -1);
        else
            num = string.sub(numTemp, 1, numStartIndex - 1);
        end

        spliceNum(num);
    end

    spliceNum(numStr);

    local originalPrice = FormulaM.invoke("CALC_LIMIT_GIFT_ORIGINAL_PRICE", value, num);

    -- 非数字的位置还原
    local notNumKeys = table.keys(notNumMap);
    if #notNumKeys > 0 then
        table.sort(notNumKeys);

        local function restoreNum(numTemp)
            local numTempStr = tostring(numTemp);

            for k = 1, #notNumKeys do
                local numLen = string.len(numTempStr);

                local notNumKey = notNumKeys[k];
                local notNumValue = notNumMap[notNumKey];

                numTempStr = string.sub(numTempStr, 1, -notNumKey - 1) .. notNumValue .. string.sub(numTempStr, numLen - notNumKey + 1, -1);
            end

            return numTempStr;
        end

        originalPrice = restoreNum(originalPrice);
        num = restoreNum(num);
    end

    local okText = num;

    -- originalPrice如果不是以数字开头的需要补上0
    numStartIndex = string.find(originalPrice, "%d");

    if numStartIndex > 1 then
        originalPrice = "0" .. originalPrice;
    end

    local originalText = originalPrice;

    if not isOverseaPackage() then
        -- 国内的需要显示小数点后两位
        local inlandNum = string.gsub(num, ",", "");
        local inlandoriginalPrice = string.gsub(originalPrice, ",", "");
        okText = string.format("%0.2f", tonumber(inlandNum));
        originalText = string.format("%0.2f", tonumber(inlandoriginalPrice));
    elseif isBuyGemByPt() then
        -- 如果通过pt购买的话，需要添加pt单位
        okText = tostring(num) .. " pt";

        originalText = tostring(originalPrice) .. " pt";
    end

    okText = string.format(currencyStr, tostring(okText));

    originalText = string.format(currencyStr, tostring(originalText));

    return okText, originalText;
end


-- 解析商城描述
function parseGoodsDesc(descStr, goodsInfo)
    local desc = descStr;
    local goodsId = goodsInfo.id;

    -- 购买确认
    if string.match(desc, "{goods}") ~= nil then
        -- 动态显示商品名称
        desc = string.gsub(desc, "{goods}", goodsInfo["name"]);
    end

    local price = goodsInfo["price"];
    if string.match(desc, "{rmb}") ~= nil then
        -- 动态显示rmb价格
        desc = string.gsub(desc, "{rmb}", price["rmb"]);
    end

    if string.match(desc, "{orig_rmb}") ~= nil then
        -- 动态显示rmb原价
        desc = string.gsub(desc, "{orig_rmb}", price["orig_rmb"]);
    end

    if string.match(desc, "{gem}") ~= nil then
        -- 动态显示钻石价格
        desc = string.gsub(desc, "{gem}", price["gem"]);
    end

    if string.match(desc, "{money}") ~= nil then
        -- 动态显示金币数量
        desc = string.gsub(desc, "{money}", tostring(FormulaM.invoke("CALC_GOODS_BONUS_MONEY", ME.user, goodsInfo["bonus_args"])));
    end

    if string.match(desc, "{debris}") ~= nil then
        -- 动态显示古遗迹碎片数量
        desc = string.gsub(desc, "{debris}", tostring(FormulaM.invoke("CALC_GOODS_BONUS_DEBRIS", ME.user, goodsInfo["bonus_args"])));
    end

    if string.match(desc, "{currency}") ~= nil then
        -- 动态显示商品价格
        local sku = MarketM.querySku(goodsId);
        if type(sku) ~= 'string' then
            price = string.format("$%.2f", table.values(goodsInfo["price"])[1]);
        else
            price = MarketM.getPriceBySku(sku);
        end

        desc = string.gsub(desc, "{currency}", tostring(price));

        -- 转换美元
        changeDollerCharacter(desc);
    end

    if string.match(desc, "{life}") ~= nil then
        -- 动态显示体力
        desc = string.gsub(desc, "{added_life}", tostring(math.min(9, ME.user.dbase:query("life", 0) + ME.user:getMaxLife())))
        desc = string.gsub(desc, "{life}", tostring(ME.user.dbase:query("life", 0)));
        desc = string.gsub(desc, "{max_life}", tostring(ME.user:getMaxLife()));
    end

    local canBuyTimes = goodsInfo["one-off"] or 0;
    local alreadyBuyInfo = ME.user.dbase:query("market_buy", {});
    local alreadyBuyTimes;
    if string.match(desc, "{can_buy_times}") ~= nil then
        -- 显示总可购买次数
        desc = string.gsub(desc, "{can_buy_times}", tostring(canBuyTimes));
    end

    if string.match(desc, "{rest_buy_times}") ~= nil then
        -- 已购买次数
        if alreadyBuyInfo[goodsId] == nil then
            alreadyBuyTimes = 0;
        else
            alreadyBuyTimes = alreadyBuyInfo[goodsId]["buy_times"] or 0;
        end

        desc = string.gsub(desc, "{rest_buy_times}", tostring(canBuyTimes - alreadyBuyTimes));
    end

    -- 补偿奖励奖励描述
    if string.match(desc, "{compensate_desc}") ~= nil then
        local compensateDesc = getCompensateDesc(goodsId);
        desc = string.gsub(desc, "{compensate_desc}", compensateDesc);
    end

    -- 如果是充值
    if goodsInfo["classify"] == "recharge" then
        -- 计算充值可获得的钻石数量
        local bonusAmount = FormulaM.invoke("CALC_CHARGE_GEM", goodsId);
        desc = desc .. string.format(getLocStr("first_recharge"), bonusAmount);
    end

    return desc;
end

-- 计算补偿钻石奖励
function calcCompensateGem(goodsId, isPt)
    if not isPt then
        -- 不是pt购买
        return 0;
    end

    return FormulaM.invoke("CALC_PT_BUY_COMPENSATE_GEM", goodsId);
end

-- 获取补偿描述
function getCompensateDesc(goodsId)
    -- 计算补偿钻石
    local isPt = isBuyGemByPt();
    compensateGem = calcCompensateGem(goodsId, isPt);
    if compensateGem <= 0 then
        -- 没有补偿
        return "";
    end

    return string.format(getLocStr("extra_receive_gem"), compensateGem);
end

-- 判断当前是否能够根据pt值购买
function isBuyGemByPt()
    if type(PAY_SDK) == "table" and #table.keys(PAY_SDK) > 0 then
        return true;
    end

    return false;
end

-- 通过pt购买钻石
function buyGoodsByPt(goodsId)
    local ptCost = query(goodsId, "pt_price") or {};

    if #table.keys(ptCost) < 1 then
        return false;
    end

    local attrib = table.keys(ptCost)[1];
    local count = ptCost[attrib];

    if ME.user:queryAttrib(attrib) < count then
        -- 通知充值界面打开界面
        EventMgr.fire(event.OPEN_PAYMENT_WAY);
        return;
    end

    -- 扣除当前属性，直接奖励
    if not ME.user:costAttrib(attrib, count) then
        return false;
    end

    -- 购买成功 通知服务端
    if buyGoods(goodsId, true, true) then
        -- 通知服务器
        Operation.cmd_buy_goods_by_pt(goodsId);
    end

    return true;
end

-- 判断是不是充值pt的商品
function isPtRechargeGoods(goodsId)
    local classify = query(goodsId, "classify");

    if classify == "pt_recharge" then
        return true;
    end

    return false;
end

-- 判断是否显示支付宝或者微信选择页面(QC平台使用)
function isInlandPayWay()
    if WECHAT_ENABLED == 1 and PUBLISH_PLATFORM == PUBLISH_TYPE_QC then
        return true;
    end

    return false;
end

-- 获取活动本对应商品
function getActivityList(activityId, shipSkill)
    activityId = activityId or ActivityDungeonM.getCurrentActivityId();

    local list = getGoodsByType("activity");
    local finalList = {};
    for _, info in pairs(list) do
        local dbase = info["dbase"];
        local pieceId = info.bonus[1][2];
        local petId = PetM.getPetidByPieceid(pieceId);
        -- 有冈布奥/是对应的活动
        if ME.user.pets[petId] then
            local activitys = dbase["activity_id"];
            if type(activitys) == "number" and activitys == activityId then
                table.insert(finalList, info);
            elseif type(activitys) == "table" and table.indexOf(activitys, activityId) >= 0 then
                table.insert(finalList, info);
            end
        end
    end

    -- 添加上秘宝
    if shipSkill == true then
        finalList = table.append(finalList, getActivityShipSkillList(activityId))
    end

    return finalList;
end

-- 获得活动对应的秘宝
function getActivityShipSkillList(activityId)
    activityId = activityId or ActivityDungeonM.getCurrentActivityId();
    local finalList = {};
    local shipSkillList = getGoodsByType("ship_skill") or {};

    for _, skillInfo in pairs(shipSkillList) do
        local skillDbase = skillInfo["dbase"];
        local activitys = skillDbase["activity_id"]

        if (type(activitys) == "number" and activitys == activityId) or
            (type(activitys) == "table" and table.indexOf(activitys, activityId) >= 0) then
            local petId = skillDbase["pet_id"];

            -- 宠物限制（如果有配置冈布奥ID的话，必须拥有对应冈布奥）
            if petId == nil then
                table.insert(finalList, skillInfo);
            elseif type(petId) == "number" then
                local pet = PetM.getMyPet(petId);
                -- 没有该英雄
                if pet then
                    table.insert(finalList, skillInfo);
                end
            end
        end
    end

    return finalList;
end

-- 获取开放通关关卡
function getOpenDungeon()
    return OPEN_DUNGEON;
end

-- 计算充值总额（美金）
function calcTotalRecharge()
    local marketBuyInfo = ME.user.dbase:query("market_buy");
    if type(marketBuyInfo) ~= "table" then
        return 0;
    end

    local total = 0;
    for goodsId, timesInfo in pairs(marketBuyInfo) do
        if isRmbBuy(goodsId) then
            local dbase = query(goodsId, "dbase");
            local price = GOODS_PRICE_MAP[goodsId] or 0;
            total = total + price;
        end
    end

    return total;
end

-- 获取最近一次充值的时间
function getLastRechargeTime()
    return ME.user.dbase:query("last_charge_time");
end

-- 获取最近一次购买的内购商品ID
function getLastRechargeGoods()
    return ME.user.dbase:query("last_charge_goods");
end

-- 获取玩家的充值数
function getChargeAmount()
    return ME.user.dbase:query("user_charge_amount", 0);
end

-- 获取商城可售的碎片列表
function getMarketSellScrapList(distinguishCamp)
    -- 活动和碎片
    local list = table.append(getActivityList(), getGoodsByType("scrap"));

    local finalList = {};
    for _, info in pairs(list) do
        repeat
            local dbase = info["dbase"];
            if dbase["activity_id"] and dbase["market_show"] ~= 1 then
                -- 活动的必须要配置market_show才允许显示在商城里
                break;
            end

            -- 若玩家已有该冈布奥，则加入碎片
            local pieceId = info.bonus[1][2];
            local petId = PetM.getPetidByPieceid(pieceId);
            if ME.user.pets[petId] then
                if distinguishCamp == true then
                    local camp = iif(CampM.getPetCamp(petId) == -1, 1, CampM.getPetCamp(petId));
                    finalList[camp] = finalList[camp] or {};
                    table.insert(finalList[camp], info);
                else
                    table.insert(finalList, info);
                end
            end
        until true
    end

    return finalList;
end